OS (25) 데이터 무결성과 보호 - 디스크는 믿을 수 없다
데이터 무결성과 보호: 디스크는 믿을 수 없다 (Data Integrity & Protection)
백엔드 개발이나 시스템 프로그래밍을 하다 보면 우리는 암묵적인 전제를 하나 가집니다. "내가 파일 시스템에 write()를 성공했다면, 그 데이터는 안전하게 저장되었고 언제든 read() 하면 똑같은 값을 돌려줄 것이다."라는 전제죠.
하지만 운영체제와 스토리지의 깊은 곳을 들여다보면, 이 전제는 사실 거짓 에 가깝습니다. 디스크는 완벽하지 않으며, 생각보다 자주 데이터를 잃어버리거나 망가뜨립니다.
오늘은 OSTEP(Operating Systems: Three Easy Pieces)의 데이터 무결성과 보호(Data Integrity and Protection) 챕터를 통해, 신뢰할 수 없는 하드웨어 위에서 파일 시스템이 어떻게 데이터의 안전성 을 보장하는지 깊이 있게 파헤쳐 보겠습니다. RAID를 넘어, 실제 디스크 오류 모델과 이를 해결하기 위한 체크섬(Checksum), 그리고 잃어버린 쓰기(Lost Write) 문제까지 다룹니다.
1. 핵심 질문: 데이터 무결성을 어떻게 보장하는가?
저장 장치에 쓴 데이터가 안전하게 보호되고 있다는 것을 시스템은 어떻게 보장할까요? 만약 디스크가 몰래 데이터를 바꾼다면(Bit rot), 우리는 그것을 어떻게 알아채고 복구해야 할까요?
이 질문에 답하기 위해서는 먼저 '디스크가 어떻게 고장 나는가' 에 대한 모델링이 필요합니다.
2. 디스크 오류 모델: Fail-Stop vs Fail-Partial
과거의 RAID 시스템을 공부할 때는 디스크 오류 모델이 단순했습니다. 이를 실패-시-멈춤(Fail-Stop) 모델이라고 합니다.
- Fail-Stop: 디스크가 작동하거나, 아니면 아예 고장 나서 멈추거나 둘 중 하나입니다. 고장 여부를 시스템이 즉시 알 수 있으므로, RAID는 망가진 디스크를 갈아 끼우고 패리티를 이용해 복구하면 그만이었습니다.
하지만 현대의 디스크는 훨씬 교활하게 고장 납니다. 이를 부분-실패(Fail-Partial) 모델이라고 부릅니다.
- Fail-Partial: 디스크 전체는 정상적으로 동작하는 것처럼 보입니다.
mount도 잘 되고, 대부분의 파일도 잘 읽힙니다. 하지만 특정 블록(Block)에 접근할 때만 오류가 발생하거나, 심지어 잘못된 데이터 를 리턴하기도 합니다.
이 부분 실패 모델에서 발생하는 오류는 크게 두 가지로 나뉩니다.
2.1. 숨어있는 섹터 에러 (Latent Sector Error, LSE)
LSE는 디스크의 특정 섹터나 섹터 그룹이 물리적으로 손상되어 읽을 수 없는 상태를 말합니다.
- 원인: 디스크 헤드가 표면에 닿는 '헤드 크래시(Head Crash)'나 강한 방사선에 의한 비트 반전 등이 원인입니다.
- 증상: 다행히 디스크 내부의 ECC(Error Correcting Code)가 이를 감지합니다. 하지만 ECC로 복구할 수 없을 정도로 손상되었다면, 디스크는 OS에게 나 이 블록 못 읽겠어 (Read Error) 라고 명시적으로 알려줍니다.
- 특징:
- 오래된 드라이브일수록 에러율이 증가합니다.
- 디스크 용량이 클수록 LSE 발생 확률도 비례해서 늘어납니다.
- 한 번 LSE가 발생한 디스크는 또 다른 LSE를 만들어낼 확률이 높습니다(공간/시간 지역성).
2.2. 블록 손상 (Block Corruption) - 조용한 오류(Silent Fault)
이것이 진짜 무서운 놈입니다. 데이터가 망가졌는데 디스크가 그 사실을 모르고, 에러도 리턴하지 않는 경우 입니다.
- 원인: 디스크 펌웨어의 버그로 데이터를 엉뚱한 위치에 썼거나, 전송 버스(Bus) 상의 오류로 이동 중에 데이터가 변질된 경우입니다.
- 증상: 사용자는 데이터를 요청합니다. 디스크는 아무런 오류 메시지 없이 데이터를 줍니다. 하지만 그 데이터는 내가 저장했던 데이터가 아닙니다.
- 위험성: 이를 조용한 오류(Silent Fault) 라고 합니다. 시스템은 정상이라고 생각하고 이 망가진 데이터를 기반으로 연산을 수행하거나, DB에 저장해버립니다. 재앙의 시작이죠.
연구 결과에 따르면, 저가형 SATA 드라이브뿐만 아니라 고가의 SCSI 드라이브에서도 이러한 현상은 드물지만 분명히 발생한다고 합니다.
3. LSE(숨어있는 섹터 에러) 처리하기
LSE는 그나마 대처하기 쉽습니다. 디스크가 "에러 발생!"이라고 알려주기 때문입니다.
해결책: 중복(Redundancy) 활용
저장 시스템이 블록을 읽으려다 에러를 받으면, 시스템은 미리 저장해 둔 중복 정보 를 활용해 데이터를 복구합니다.
- RAID 1 (Mirroring): 다른 미러 디스크에 있는 복사본을 읽습니다.
- RAID 4/5 (Parity): 패리티 그룹 내의 다른 디스크들을 읽어 XOR 연산으로 깨진 블록을 재계산(Reconstruct)합니다.
RAID-DP (Double Parity)의 등장
LSE가 빈번해지면서 RAID 5에도 문제가 생겼습니다. 만약 디스크 하나가 완전히 고장(Fail-Stop) 나서 재구성(Reconstruct)을 하고 있는데, 나머지 멀쩡한 줄 알았던 디스크 중 하나에서 LSE가 발견된다면? 복구할 정보가 부족해 데이터가 영영 손실됩니다. 이를 해결하기 위해 NetApp의 RAID-DP 같은 시스템은 두 개의 패리티 디스크 를 사용하여, 재구성 도중 LSE가 발생해도 데이터를 복구할 수 있도록 설계되었습니다. 비용은 더 들지만, 데이터 안전성은 훨씬 높아집니다.
4. 손상 검출의 핵심: 체크섬 (Checksum)
가장 어려운 문제인 '조용한 데이터 손상(Block Corruption)' 으로 넘어가 봅시다. 디스크가 뻔뻔하게 잘못된 데이터를 줄 때, OS는 이를 어떻게 감지할까요?
정답은 체크섬(Checksum) 입니다.
4.1. 체크섬이란?
데이터 청크(예: 4KB 블록)의 내용을 요약한 작은 데이터(예: 4~8 바이트)입니다. 데이터를 쓸 때 체크섬을 계산해서 같이 저장하고, 읽을 때 다시 계산해서 저장된 값과 비교합니다.
저장된_체크섬 == 계산된_체크섬: 데이터 무결성 인정.저장된_체크섬 != 계산된_체크섬: 데이터가 변조됨(손상).
4.2. 체크섬 함수의 종류
체크섬 함수는 속도와 충돌(Collision, 다른 데이터인데 같은 체크섬이 나오는 확률) 가능성 사이에서 트레이드오프가 있습니다.
-
XOR 체크섬:
- 가장 간단합니다. 데이터를 4바이트 단위로 쪼개서 세로로 XOR 연산을 합니다.
- 장점: 매우 빠릅니다.
- 단점: 같은 열(column)의 비트가 짝수 개만큼 바뀌면 변경 사실을 감지하지 못합니다. (예: 0->1, 1->0으로 두 개가 바뀌면 XOR 결과는 같음). 신뢰성이 낮습니다.
-
덧셈(Addition) 체크섬:
- 데이터 청크를 2의 보수 덧셈으로 다 더하고 오버플로우는 버립니다.
- 장점: 역시 빠릅니다.
- 단점: 데이터의 값은 그대로인데 순서가 바뀌거나(Shift), 특정 패턴으로 데이터가 바뀌면 감지하지 못할 수 있습니다.
-
플렛처(Fletcher) 체크섬:
- XOR나 덧셈보다 조금 더 정교합니다. 두 개의 체크 바이트(, )를 운용합니다.
- 위치에 따른 가중치가 부여되는 효과가 있어, 단순 덧셈보다 훨씬 강력하게 에러를 검출합니다.
-
CRC (Cyclic Redundancy Check):
- 데이터를 거대한 이진수로 보고, 미리 약속된 특정 값(Generator Polynomial)으로 나눈 나머지 를 체크섬으로 씁니다.
- 네트워크와 스토리지 분야에서 가장 널리 쓰입니다.
- 이진 나눗셈(Modulo-2 연산)은 하드웨어로 구현하기 매우 효율적이며, 연속적인 비트 오류(Burst Error)를 검출하는 데 탁월합니다.
일반적으로 스토리지 시스템은 충돌 확률을 최소화하면서도 계산 비용이 적당한 CRC 계열이나 Fletcher 알고리즘을 많이 사용합니다. 물론 보안이 중요한 곳에서는 MD5나 SHA 같은 암호학적 해시를 쓰기도 하지만, 파일 시스템의 속도를 위해선 너무 무겁습니다.
5. 체크섬을 디스크에 어떻게 배치할까? (Layout)
체크섬을 계산하는 건 알겠는데, 이걸 디스크 어디에 저장해야 할까요?
5.1. 섹터 내 포함 방식 (The 520-byte Sector)
가장 직관적인 방법은 각 섹터(512바이트) 바로 뒤에 체크섬(8바이트)을 붙이는 것입니다.
- 문제: 일반적인 하드디스크는 512바이트 단위로만 쓰기가 가능합니다. 520바이트 쓰기는 불가능합니다.
- 해결: 이를 위해 엔터프라이즈급 디스크 제조사는 520바이트 포맷 을 지원하기도 합니다. 하드웨어 레벨에서 8바이트의 여유 공간을 주는 것이죠.
5.2. 별도의 체크섬 블록 방식
일반 소비자용 디스크(SATA 등)를 쓴다면 파일 시스템이 알아서 해야 합니다. 보통은 데이터 블록 개당 체크섬 블록 1개를 할당하여 따로 저장합니다.
- 구조:
[Data 0] [Data 1] ... [Data n] [Checksum Block] - 장점: 모든 디스크에서 사용 가능합니다.
- 단점 (치명적): 데이터를 갱신할 때 오버헤드가 큽니다.
- 예를 들어
Data 1을 수정하려면,Checksum Block을 읽어서 기존Data 1의 체크섬을 뺍니다.- 새로운
Data 1의 체크섬을 더합니다. Data 1을 쓰고,Checksum Block도 다시 써야 합니다.
- 즉, 한 번의 쓰기를 위해 읽기 1회 + 쓰기 2회 가 발생합니다. (Read-Modify-Write). 이 문제를 완화하기 위해 로그 구조 파일 시스템(LFS)이나 Copy-on-Write(CoW) 방식을 쓰는 ZFS 같은 파일 시스템이 유리합니다.
- 예를 들어
6. 더 기괴한 오류들: 잘못된 위치 기록 & 기록 손실
단순히 데이터가 깨지는 것(Corruption) 외에도, 디스크 컨트롤러나 펌웨어의 버그로 인해 발생하는 황당한 오류들이 더 있습니다.
6.1. 잘못된 위치에 기록 (Misdirected Write)
데이터는 정상입니다. 체크섬도 그 데이터에 맞게 잘 계산되었습니다. 그런데 이 데이터가 엉뚱한 주소 에 저장되는 경우입니다.
- 시나리오: 디스크 0번의 100번 섹터에 써야 할 데이터를 디스크 1번의 100번 섹터에 쓰거나, 같은 디스크의 200번 섹터에 덮어써 버립니다.
- 문제: 나중에 200번 섹터를 읽으면, 체크섬 검사를 통과합니다(데이터와 체크섬이 세트로 엉뚱한 곳에 갔으니까요). 하지만 내용은 사용자가 원한 게 아닙니다.
- 해결책: 물리적 식별자(Physical Identity).
- 체크섬에 데이터의 해시값뿐만 아니라, 이 데이터는 디스크 X의 섹터 Y에 있어야 한다 는 정보를 포함시킵니다.
- 읽을 때,
기대했던 위치(X, Y)와체크섬에 기록된 위치(X', Y')가 다르면 에러로 간주합니다.
6.2. 기록 작업의 손실 (Lost Write)
OS는 쓰기 명령을 내렸고, 디스크 컨트롤러는 "완료!"라고 응답했습니다. 하지만 실제로는 디스크에 기록되지 않은 경우입니다.
- 시나리오: 버퍼에만 있고 플러시되지 않았거나 펌웨어 버그로 증발했습니다. 나중에 해당 블록을 읽으면, 아주 예전의 데이터(Old Data) 가 읽힙니다.
- 문제: 예전 데이터도 그 당시에는 유효했으므로, 그 데이터 안에 들어있는 체크섬도 유효합니다(Valid Checksum). 따라서 단순 체크섬 검사로는 이를 잡아낼 수 없습니다.
- 해결책 1: 쓰기 후 읽기 (Read-After-Write). 쓰고 나서 바로 읽어서 확인합니다. 하지만 너무 느려서 실제로 쓰지 않습니다.
- 해결책 2: ZFS 방식 (Parent Checksum).
- 이것이 핵심입니다. 체크섬을 데이터와 함께 저장하지 않고, 그 데이터를 가리키는 포인터(아이노드나 간접 블록)에 저장 합니다.
Inode가Data Block A를 가리킬 때,Inode안에Checksum(A)를 저장해 둡니다.- 만약
Data Block A의 쓰기가 손실되어 옛날 데이터가 남아있다면,Inode에 저장된 최신 체크섬과 옛날 데이터의 내용이 일치하지 않으므로 검출할 수 있습니다. - 이 방식은 데이터 트리의 상위 노드가 하위 노드의 무결성을 보장하는 Merkle Tree 구조를 형성합니다.
7. 스크러빙 (Scrubbing): 데이터 목욕 시키기
체크섬 시스템을 잘 갖췄다고 해도 문제가 하나 남습니다. "우리가 그 데이터를 읽지 않으면, 망가졌는지 어떻게 알지?" 대부분의 데이터는 자주 접근되지 않습니다(Cold Data). 그 사이에 비트가 썩어버리면(Bit rot), 나중에 중요할 때 읽으려다 낭패를 봅니다. 복구하려고 해도 미러링된 사본까지 같이 썩어있을 수도 있죠.
그래서 시스템은 주기적으로 스크러빙(Disk Scrubbing) 을 수행합니다.
- 동작: 백그라운드에서 주기적으로 시스템의 모든 블록 을 읽습니다.
- 검증: 읽은 데이터의 체크섬을 계산해보고 유효한지 확인합니다.
- 복구: 만약 깨진 데이터를 발견하면, 유효한 사본(RAID 등)을 이용해 즉시 복구합니다.
- 보통 매일 밤이나 주말 새벽 등 시스템 부하가 적은 시간에 수행합니다.
8. 비용 (Overhead) 분석
이 모든 데이터 보호 기술에는 당연히 비용이 따릅니다. "세상에 공짜 점심은 없다(TNSTAAFL)"는 말은 시스템 프로그래밍에서도 진리입니다.
-
공간 오버헤드:
- 디스크 공간: 4KB 블록당 8바이트 체크섬이라면 약 0.19%의 공간을 씁니다. 무시할 만한 수준입니다.
- 메모리 공간: 읽은 데이터를 검증하기 위해 메모리에 체크섬을 올려야 하므로 약간의 메모리를 더 씁니다.
-
시간 오버헤드 (더 중요):
- CPU: 모든 읽기/쓰기 작업마다 수학 연산(체크섬 계산)을 해야 합니다. 이를 줄이기 위해 OS는 데이터 복사(Buffer Copy)를 할 때 체크섬 계산을 동시에 수행하는 최적화를 하기도 합니다.
- I/O: 체크섬이 데이터와 분리되어 저장된 경우, 체크섬 블록을 읽고 쓰기 위한 추가적인 I/O가 발생합니다. 스크러빙 또한 I/O 대역폭을 잡아먹습니다.
하지만 데이터가 소리 없이 망가져서 겪게 될 재앙(DB 손상, 파일 깨짐)에 비하면, 이 정도 오버헤드는 아주 저렴한 보험료 라고 할 수 있습니다.
9. 요약 및 결론
오늘 학습한 내용을 개발자의 언어로 요약해 보겠습니다.
- 디스크를 믿지 마십시오.
write()성공이 데이터의 영속성을 보장하지 않습니다. 디스크는 거짓말을 할 수 있습니다(Fail-Partial). - LSE(물리적 배드 섹터)는 흔합니다. RAID 같은 중복(Redundancy) 시스템이 필수적입니다.
- 조용한 부패(Silent Corruption)가 가장 위험합니다. 디스크가 에러를 뱉지 않고 엉뚱한 값을 줍니다. 이를 잡으려면 체크섬(Checksum) 이 필수입니다.
- 단순 체크섬으로 부족한 경우가 있습니다.
- Misdirected Write: 물리적 ID(디스크 번호, 오프셋)를 체크섬에 포함해야 합니다.
- Lost Write: 체크섬을 데이터 자체가 아닌, 부모 노드(Inode 등)에 저장해야 합니다(ZFS 스타일).
- 데이터도 관리가 필요합니다. 주기적인 스크러빙(Scrubbing)으로 잠재된 에러를 미리미리 고쳐야 합니다.
